<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            list-style: none;
        }

        .box {
            width: 1000px;
            height: 800px;
            margin: 30px auto;
            display: flex;
            border: 1px solid;
        }

        #myCanvas {
            margin: auto;
        }

        #myCanvas:focus {
            outline: none;
        }
    </style>
</head>

<body>
    <div class="box">
        <canvas id="myCanvas" height="750" width="950" tabindex="333"></canvas>
    </div>
    位置：<div class="location"></div>
    速度：<div class="ver"></div>
</body>
<script>
    const locationDom = document.querySelector('.location')
    const vDom = document.querySelector('.ver')
    /** @type { HTMLCanvasElement } */
    const myCanvas = document.getElementById('myCanvas');
    const ctx = myCanvas.getContext('2d');

    let xx = 100  // x 初始位置>> 100px
    let yy = 100  // y 初始位置>> 100px
    let vx = 3  // x 速率>> 100px 移动 30px，数字越大，移动、跟随速度越快
    let vy = 3  // y 速率>> 100px 移动 30px，数字越大，移动、跟随速度越快
    // vx === vy 的时候。能够实现一次函数效果
    myCanvas.focus()    // 先聚焦，为键盘事件铺垫
    ctx.save()
    ctx.beginPath();
    ctx.arc(xx, yy, 50, 0, 2 * Math.PI)
    ctx.fillStyle = "#9f0"
    ctx.fill()
    ctx.strokeStyle = "#06f"
    locationDom.innerHTML = xx + ',' + yy
    vDom.innerHTML = 'vx：' + 0 + ', vy：' + 0
    ctx.stroke()
    ctx.closePath()
    ctx.restore()

    let ids = null

    myCanvas.onclick = function (e) {
        // 转折点: [xx, yy]
        animationCircle(ctx, e.offsetX, e.offsetY, [xx, yy])
    }

    myCanvas.onmousemove = (e) => {
        animationCircle(ctx, e.offsetX, e.offsetY, [xx, yy])
    }

    myCanvas.onkeydown = (e) => {
        // 转折点: [xx, yy]
        if (e.keyCode === 87 || e.keyCode === 38) {
            // 上
            animationCircle(ctx, xx, yy - 50, [xx, yy])
        }
        else if (e.keyCode === 65 || e.keyCode === 37) {
            // 左
            animationCircle(ctx, xx - 50, yy, [xx, yy])
        }
        else if (e.keyCode === 68 || e.keyCode === 39) {
            // 右
            animationCircle(ctx, xx + 50, yy, [xx, yy])
        }
        else if (e.keyCode === 83 || e.keyCode === 40) {
            // 下
            animationCircle(ctx, xx, yy + 50, [xx, yy])

        }
    }

    function animationCircle(ctx, targetX, targetY, turnPoint) {
        ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
        if (ids) {
            window.cancelAnimationFrame(ids)
        }
        // 小于的这个数相当于运动的最小分辨率（就是在一帧内移动的距离），由速度决定
        if (Math.abs(xx - targetX) < Math.abs(vx * ((targetX - turnPoint[0]) / 100))) {
            xx = targetX
        } else {
            xx += vx * ((targetX - turnPoint[0]) / 100)
        }
        if (Math.abs(yy - targetY) < Math.abs(vy * ((targetY - turnPoint[1]) / 100))) {
            yy = targetY
        } else {
            yy += vy * ((targetY - turnPoint[1]) / 100)
        }
        drawCircle(ctx, xx, yy)
        vDom.innerHTML = `vx：${vx * ((targetX - turnPoint[0]) / 100)}，vy：${vy * ((targetY - turnPoint[1]) / 100)}`
        if (Math.abs(xx - targetX) <= vx && Math.abs(yy - targetY) <= vy) {
            window.cancelAnimationFrame(ids)
            ids = null
            return false;
        }
        ids = window.requestAnimationFrame(() => animationCircle(ctx, targetX, targetY, turnPoint));
    }

    function drawCircle(ctx, x, y, r = 50) {
        ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)
        ctx.save()
        ctx.beginPath()

        ctx.arc(x, y, r, 0, 2 * Math.PI)
        ctx.fillStyle = "#9f0"
        ctx.fill()
        ctx.strokeStyle = "#06f"
        ctx.stroke()

        locationDom.innerHTML = x + ',' + y

        ctx.closePath()
        ctx.restore()
    }

</script>

</html>